package org.manon.json.asm;

import static org.manon.json.asm.ASMUtil.TYPE_BOOLEAN;
import static org.manon.json.asm.ASMUtil.TYPE_BYTE_ARRAY;
import static org.manon.json.asm.ASMUtil.TYPE_FILTER;
import static org.manon.json.asm.ASMUtil.TYPE_INT;
import static org.manon.json.asm.ASMUtil.TYPE_MAP;
import static org.manon.json.asm.ASMUtil.TYPE_OBJECT;
import static org.manon.json.asm.ASMUtil.TYPE_OUTPUTSTREAM;
import static org.manon.json.asm.ASMUtil.TYPE_SET;
import static org.manon.json.asm.ASMUtil.TYPE_STRING;
import static org.manon.json.asm.ASMUtil.TYPE_VOID;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Iterator;

import org.manon.json.JSONConstants;
import org.manon.json.JSONException;
import org.manon.json.analyse.ClassRegistry;
import org.manon.json.analyse.TreeNode;
import org.manon.json.asm.ser.CreateConstructorTemplate;
import org.manon.json.asm.ser.CreateFieldTemplate;
import org.manon.json.asm.ser.CreateMethodObjectTemplate;
import org.manon.json.filter.Filter;
import org.manon.json.meta.Entity;
import org.manon.json.meta.MetaProperty;
import org.manon.json.meta.MetaPropertyAssociation;
import org.manon.json.meta.MetaPropertyCollection;
import org.manon.json.meta.MetaPropertyDiscriminator;
import org.manon.json.meta.MetaPropertyGeneric;
import org.manon.json.meta.MetaPropertyMap;
import org.manon.json.util.ClassUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;


/**
 * Generate Serialzer with ASM.
 * 
 * @author <a href="mailto:jacques.militello@gmail.com">Jacques Militello</a>
 */
public final class ASMGenerator4Serializer extends ASMBaseGenerator{

	/**
	 * Suffix for class name of serializer.
	 */
	public static final String FIELD_SUFFIX = "_ser_inst";
	
	/**
	 * Prefix for discriminator field.
	 */
	public static final String FIELD_DISCRIMINATOR_PREFIX = "__DI__";
	
	private final MethodVisitorTemplate<ASMContextSerializer> _templateFields = new CreateFieldTemplate(this);
	private final MethodVisitorTemplate<ASMContextSerializer> _templateConstructor = new CreateConstructorTemplate(this);
	private final MethodVisitorTemplate<ASMContextSerializer> _templateMethodObject = new CreateMethodObjectTemplate();
	
	ASMGenerator4Serializer(ClassRegistry classRegistry) {
		super(classRegistry);
	}
	
	void generateClassHeader(ClassRegistry classRegistry,Entity entity,ClassWriter classWriter) {
		
		final String[] path = ClassUtils.path(entity.getEntityClass());
		final boolean hasChild = classRegistry.hasChild(entity);
		
		if (path.length > 1) {
			final Entity parentEntity = classRegistry.search(Arrays.copyOfRange(path, 0, path.length-1));
			classWriter.visit(ASMUtil.JAVA_VERSION,	Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER + ((hasChild) ? 0 : Opcodes.ACC_FINAL),
					// the internal name of the class
					entity.getInternalClassNameForSerializer(),
					// the signature of this class
					null,
					//Type.getDescriptor(ASMSerializer.class) + "<L" +Type.getInternalName(entity.getEntityClass()) +";>;",
					// the internal of name of the super class
					parentEntity.getInternalClassNameForSerializer() ,
					// the internal names of the class's interfaces
					null);	
			
		} else {
			classWriter.visit(ASMUtil.JAVA_VERSION, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER + ((hasChild) ? 0 : Opcodes.ACC_FINAL),
					// the internal name of the class
					entity.getInternalClassNameForSerializer(),
					// the signature of this class
					Type.getDescriptor(ASMSerializer.class) + "<L" +Type.getInternalName(entity.getEntityClass()) +";>;",
					// the internal of name of the super class
					Type.getInternalName(ASMSerializer.class)  ,
					// the internal names of the class's interfaces
					null);
		}
	}

	void generateBody(Entity entity,ClassWriter classWriter) {
		final ASMContextSerializer context = new ASMContextSerializer(this,classWriter,entity);
		
		_templateFields.setContext(context);
		_templateConstructor.setContext(context);
		_templateMethodObject.setContext(context);
		
		try {
			_templateFields.create();
			_templateConstructor.create();
			_templateMethodObject.create();
			new GenerateMethod(context).create();
		} catch (Exception exception) {
			throw new JSONException("Failed to build serialiser for entity [" + entity + "]",exception);
		}
	}
	
	private void generateMetohdInner(ASMContextSerializer context) throws Exception {
		
		final MethodVisitor mv = context.getClassWriter().visitMethod(Opcodes.ACC_PROTECTED,"innerSerialize",
				Type.getMethodDescriptor(TYPE_BOOLEAN, TYPE_OUTPUTSTREAM,Type.getType(context.getEntity().getEntityClass()),TYPE_FILTER,TYPE_SET , TYPE_BOOLEAN),	null,
				new String[]{Type.getInternalName(IOException.class)});
		
		mv.visitCode();
		
		final String[] path = ClassUtils.path(context.getEntity().getEntityClass());
		
		if (path.length > 1) {
			
			final Entity parent = getRegistry().search(Arrays.copyOfRange(path, 0, path.length-1));
			
			int varContext = 5;
//			// discriminator in first position.
			
			MetaPropertyDiscriminator discriminator = findMetaPropertyDiscriminator(context.getEntity());
			
			if (discriminator != null) {
				context.setStackCount(4);
				writeDiscriminator(context,discriminator,mv);
				varContext = 4;
			}
			
			
			// call super;
			mv.visitVarInsn(Opcodes.ALOAD, 0);
			mv.visitVarInsn(Opcodes.ALOAD, 1);
			mv.visitVarInsn(Opcodes.ALOAD, 2);
			mv.visitVarInsn(Opcodes.ALOAD, 3);
			mv.visitVarInsn(Opcodes.ALOAD, 4);
			mv.visitVarInsn(Opcodes.ILOAD, 5);
			mv.visitMethodInsn(Opcodes.INVOKESPECIAL,parent.getInternalClassNameForSerializer(),"innerSerialize",
					Type.getMethodDescriptor(TYPE_BOOLEAN, TYPE_OUTPUTSTREAM,Type.getType(parent.getEntityClass()),TYPE_FILTER,TYPE_SET,TYPE_BOOLEAN));
			mv.visitVarInsn(Opcodes.ISTORE, varContext+1);
			context.setStackCount(varContext);
			writeProperties(context,mv,false);
			
					
		} else {
			context.setStackCount(4);
			writeProperties(context,mv,true);
		}
		
		
		mv.visitVarInsn(Opcodes.ILOAD, context.getFlagCount());
		//mv.visitVarInsn(Opcodes.ILOAD, 5);
		mv.visitInsn(Opcodes.IRETURN);
		mv.visitMaxs(1, 1);
		mv.visitEnd();
	}
	
	private MetaPropertyDiscriminator findMetaPropertyDiscriminator(Entity entity) {

		if (entity.getProperties().size() == 0) {
			return null;
		}
		
		for (MetaProperty property : entity.getProperties()) {
			if (property instanceof MetaPropertyGeneric) {
				continue;
			}
			if (property instanceof MetaPropertyDiscriminator) {
				return (MetaPropertyDiscriminator)property;
			}
			return null;
		}
		
		return null;
	}

	private void writeGenericObjectWithLength(MethodVisitor mv,String staticFieldName,int length) {
		// 	os.write(<staticFieldName>,0,<length>);
		mv.visitVarInsn(Opcodes.ALOAD, 1);
		mv.visitFieldInsn(Opcodes.GETSTATIC, Type.getInternalName(JSONConstants.class), staticFieldName, Type.getDescriptor(byte[].class));
		mv.visitInsn(Opcodes.ICONST_0);
		
		switch (length) {
		case 0: mv.visitInsn(Opcodes.ICONST_0);	break;
		case 1: mv.visitInsn(Opcodes.ICONST_1);	break;
		case 2: mv.visitInsn(Opcodes.ICONST_2);	break;
		case 3: mv.visitInsn(Opcodes.ICONST_3);	break;
		case 4: mv.visitInsn(Opcodes.ICONST_4);	break;
		case 5: mv.visitInsn(Opcodes.ICONST_5);	break;
		default:
			if (length <= 127) {
				mv.visitIincInsn(Opcodes.BIPUSH, length);
			} else if (length <= 32767) {
				mv.visitIincInsn(Opcodes.SIPUSH, length);
			} else {
				mv.visitLdcInsn(length);
			}
		}
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,Type.getInternalName(OutputStream.class),"write",Type.getMethodDescriptor(TYPE_VOID, TYPE_BYTE_ARRAY,TYPE_INT,TYPE_INT));
	}
	

	
	private void writeStaticField(MethodVisitor mv,ASMContextSerializer context,String staticFieldName) {
		// 	os.write(<staticFieldName>);
		mv.visitVarInsn(Opcodes.ALOAD, 1);
		mv.visitFieldInsn(Opcodes.GETSTATIC, context.getClassName(), staticFieldName, Type.getDescriptor(byte[].class));
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,TYPE_OUTPUTSTREAM.getInternalName(),"write","([B)V");
	}
	
	private void writeGetPropertiesToFilter(ASMContextSerializer context,MethodVisitor mv) {
		//final Set<String> properties = filter.getProperties(__POJO__NAME);
		// if (properties == null) {
		//		return;
		// }
		mv.visitVarInsn(Opcodes.ALOAD, 3);
		mv.visitLdcInsn(context.getEntity().getName());
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Filter.class), "getProperties", "(Ljava/lang/String;)Ljava/util/Set;");
		mv.visitVarInsn(Opcodes.ASTORE, 4);
		mv.visitVarInsn(Opcodes.ALOAD, 4);
		Label label = new Label();
		mv.visitJumpInsn(Opcodes.IFNONNULL, label);
		writeGenericObjectWithLength(mv, "EMPTY_OBJECT",2);
		mv.visitInsn(Opcodes.RETURN);
		mv.visitLabel(label);
	}
	
	
	private void writeProperties(ASMContextSerializer context,MethodVisitor mv,boolean enableDiscriminator) {
		for (MetaProperty property : context.getEntity().getProperties()) {
			switch (property.getType()) {
				case COLLECTION:
					writeCollectionProperty(context,(MetaPropertyCollection)property,mv);
					break;
				case ASSOCIATION:
					writeAssociationProperty(context,(MetaPropertyAssociation)property,mv);
					break;
				case MAP:
					writeMapProperty(context,(MetaPropertyMap)property,mv);
					break;
				case DISCRIMINATOR:
					if (enableDiscriminator) {
						writeSimpleProperty(context,property,mv);
					}
					break;
				case GENERIC:
					if (((MetaPropertyGeneric)property).isInstance()) {
						writeSimpleProperty(context,property,mv);
					}
					break;
				case SINGLE:
					writeSimpleProperty(context,property,mv);
					break;
			}
		}
	}
	
	private void writeDiscriminator(ASMContextSerializer context,MetaPropertyDiscriminator property, MethodVisitor mv) {
		mv.visitVarInsn(Opcodes.ALOAD, 0);
		mv.visitVarInsn(Opcodes.ALOAD, 1);
		mv.visitVarInsn(Opcodes.ILOAD, context.getFlagCount());
		mv.visitFieldInsn(Opcodes.GETSTATIC, context.getClassName(), "FIELD_BYTE_" + property.getName().toUpperCase(), Type.getDescriptor(byte[].class));
		
		mv.visitVarInsn(Opcodes.ALOAD, 0);			
		switch (((MetaPropertyDiscriminator) property).getDiscriminatorType()) {
		case CLASS_NAME:
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,context.getEntity().getInternalClassNameForSerializer(),"getDiscriminator",Type.getMethodDescriptor(Type.getType(byte[].class)));	
			break;
		case PROPERTY:
			mv.visitVarInsn(Opcodes.ALOAD, 2);
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,context.getEntity().getInternalClassNameForSerializer(),"getDiscriminator",
					Type.getMethodDescriptor(Type.getType(byte[].class),Type.getType(context.getEntity().getEntityClass())));	
			break;
		}			
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,context.getEntity().getInternalClassNameForSerializer(),"writeString","(Ljava/io/OutputStream;Z[B[B)Z");
		mv.visitVarInsn(Opcodes.ISTORE, context.getFlagCount());
		
	}
	
	private void writeSimpleProperty(ASMContextSerializer context,MetaProperty property, MethodVisitor mv) {
		//if (properties.contains(property.name)) {
		//	needComma = writeString(os,needComma,property.name);
		//}
		
		final Label label = new Label();

		if (context.getEntity().isFilterable()) {
			mv.visitVarInsn(Opcodes.ALOAD, 4);
			mv.visitLdcInsn(property.getLabel());
			mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, TYPE_SET.getInternalName(), "contains", "(Ljava/lang/Object;)Z");
			mv.visitJumpInsn(Opcodes.IFEQ, label);
		}
		
		mv.visitVarInsn(Opcodes.ALOAD, 0);
		mv.visitVarInsn(Opcodes.ALOAD, 1);
		mv.visitVarInsn(Opcodes.ILOAD, context.getFlagCount());
		mv.visitFieldInsn(Opcodes.GETSTATIC, context.getClassName(), "FIELD_BYTE_" + property.getName().toUpperCase(), Type.getDescriptor(byte[].class));
		
		if (property instanceof MetaPropertyDiscriminator) {
			mv.visitVarInsn(Opcodes.ALOAD, 0);			
			switch (((MetaPropertyDiscriminator) property).getDiscriminatorType()) {
			case CLASS_NAME:
				mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,context.getEntity().getInternalClassNameForSerializer(),"getDiscriminator",Type.getMethodDescriptor(Type.getType(byte[].class)));	
				break;
			case PROPERTY:
				mv.visitVarInsn(Opcodes.ALOAD, 2);
				mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,context.getEntity().getInternalClassNameForSerializer(),"getDiscriminator",
						Type.getMethodDescriptor(Type.getType(byte[].class),Type.getType(context.getEntity().getEntityClass())));	
				break;
			}			
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,context.getEntity().getInternalClassNameForSerializer(),"writeString","(Ljava/io/OutputStream;Z[B[B)Z");
			mv.visitVarInsn(Opcodes.ISTORE, context.getFlagCount());
			
			return;
		} else if (property instanceof MetaPropertyGeneric) {
			mv.visitVarInsn(Opcodes.ALOAD, 2);
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,Type.getInternalName(context.getEntity().getEntityClass()),ClassUtils.getGetter(property.getName()),"()"+Type.getDescriptor(
					((MetaPropertyGeneric)property).getParent().getReturnType()));
			mv.visitTypeInsn(Opcodes.CHECKCAST,Type.getInternalName(property.getReturnType()));
		} else {
			mv.visitVarInsn(Opcodes.ALOAD, 2);
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,Type.getInternalName(context.getEntity().getEntityClass()),ClassUtils.getGetter(property.getName()),"()"+Type.getDescriptor(property.getReturnType()));
		}
		
		
		if (ASMUtil.isNumberValue(property.getReturnType())) {
			
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
						context.getEntity().getInternalClassNameForSerializer(),
						 "writeNumber" ,
						"(Ljava/io/OutputStream;Z[B"+Type.getDescriptor(property.getReturnType())+")Z");	
			
		}
		else if (Enum.class.isAssignableFrom(property.getReturnType())) {
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
					context.getEntity().getInternalClassNameForSerializer(),
					"writeString",
					"(Ljava/io/OutputStream;Z[B"+Type.getDescriptor(Enum.class)+")Z");
		}
		else {
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
					context.getEntity().getInternalClassNameForSerializer(),
					"writeString",
					"(Ljava/io/OutputStream;Z[B"+Type.getDescriptor(property.getReturnType())+")Z");
			
		}

		mv.visitVarInsn(Opcodes.ISTORE, context.getFlagCount());
		if (context.getEntity().isFilterable()) {
			mv.visitLabel(label);
		}
		
	}

	private void checkContainsProperty(MethodVisitor mv,String propertyName,Label label) {
		//properties.contains(<property.name>)
		mv.visitVarInsn(Opcodes.ALOAD, 4);
		mv.visitLdcInsn(propertyName);
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, TYPE_SET.getInternalName(), "contains", "(Ljava/lang/Object;)Z");
		mv.visitJumpInsn(Opcodes.IFEQ, label);
		
	}

	private void checkNotNull(MethodVisitor mv,String propertyName,Label label,Class<?> entity,Class<?> returnType) {
		//	pojo.get<property.name>() != null
		mv.visitVarInsn(Opcodes.ALOAD, 2);
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(entity), ClassUtils.getGetter(propertyName), "()" + Type.getDescriptor(returnType));
		mv.visitJumpInsn(Opcodes.IFNULL, label);
	}
	
	private void checkIsEmpty(MethodVisitor mv,String propertyName,Label label,Class<?> entity,Class<?> returnType) {
		//	!pojo.get<property.name>().isEmpty()
		mv.visitVarInsn(Opcodes.ALOAD, 2);
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(entity), ClassUtils.getGetter(propertyName), "()" + Type.getDescriptor(returnType));
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(returnType), "isEmpty", "()Z");
		mv.visitJumpInsn(Opcodes.IFNE, label);
	}
	
	private void checkComma(ASMContextSerializer context,MethodVisitor mv) {
		final Label comma = new Label();
		//	if (needComma) {  os.write(COMMA); }
		mv.visitVarInsn(Opcodes.ILOAD, context.getFlagCount());
		mv.visitJumpInsn(Opcodes.IFEQ, comma);
		writeGenericObjectWithLength(mv,"COMMA",1);
		mv.visitLabel(comma);
		
	}
	
	private void checkIteratorNeedComma(MethodVisitor mv,int count) {
		final Label tt = new Label();
		mv.visitVarInsn(Opcodes.ALOAD, count);
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Iterator.class), "hasNext", "()Z");
		mv.visitJumpInsn(Opcodes.IFEQ, tt);
		writeGenericObjectWithLength(mv,"COMMA",1);
		mv.visitLabel(tt);
	}
	
	private void writeAssociationProperty(ASMContextSerializer context, MetaPropertyAssociation property, MethodVisitor mv) {
		final Label label = new Label();
		
		checkContainsProperty(mv,property.getLabel(),label);
		checkNotNull(mv, property.getName(), label, context.getEntity().getEntityClass(),property.getReturnType());
		checkComma(context, mv);
	
		// os.write(DOUBLE_QUOTE);
		writeGenericObjectWithLength(mv,"DOUBLE_QUOTE",1);
		
		// os.write(<property.name>);
		writeStaticField(mv,context,"FIELD_BYTE_" + property.getName().toUpperCase());
		
		// os.write(END_PRO);
		writeGenericObjectWithLength(mv,"PROPERTY_END",2);
		
		mv.visitVarInsn(Opcodes.ALOAD, 0);

		// mv.visitFieldInsn(Opcodes.GETFIELD, context.getClassName(), property.getName()+FIELD_SUFFIX,Type.getDescriptor(Serializer.class));
		final Entity targetEntity = findEntityClass(property.getReturnType());
		
		if (targetEntity.getCustomSerializer() != null) {
			mv.visitFieldInsn(Opcodes.GETFIELD, context.getClassName(),  property.getReturnType().getSimpleName().toLowerCase()+FIELD_SUFFIX,Type.getDescriptor(targetEntity.getCustomSerializer()));
		} else {
			mv.visitFieldInsn(Opcodes.GETFIELD, context.getClassName(), property.getReturnType().getSimpleName().toLowerCase()+FIELD_SUFFIX,ASMUtil.getDescriptorForSerializer(targetEntity));	
		}
		
		mv.visitVarInsn(Opcodes.ALOAD, 1);
		mv.visitVarInsn(Opcodes.ALOAD, 2);
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(context.getEntity().getEntityClass()), ClassUtils.getGetter(property.getName()), "()"+Type.getDescriptor(property.getReturnType()));	
		mv.visitVarInsn(Opcodes.ALOAD, 3);
		
		if (targetEntity.getCustomSerializer() != null) {
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(targetEntity.getCustomSerializer()), "serialize",
					Type.getMethodDescriptor(TYPE_VOID,TYPE_OUTPUTSTREAM, Type.getType(property.getReturnType()), TYPE_FILTER));
		} else {
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, targetEntity.getInternalClassNameForSerializer(), "serialize",
					Type.getMethodDescriptor(TYPE_VOID,TYPE_OUTPUTSTREAM, Type.getType(property.getReturnType()), TYPE_FILTER));
		}
		
		mv.visitInsn(Opcodes.ICONST_1);
		mv.visitVarInsn(Opcodes.ISTORE, context.getFlagCount());
		mv.visitLabel(label);
	}

	
	private void writeStartArray(ASMContextSerializer context,MethodVisitor mv, String propertyName) {
		// os.write(START);
		writeGenericObjectWithLength(mv,"DOUBLE_QUOTE",1);
				
		// os.write(<property.name>);
		writeStaticField(mv,context,"FIELD_BYTE_" + propertyName.toUpperCase());
				
		// os.write(END_PRO);
		writeGenericObjectWithLength(mv,"PROPERTY_END",2);
				
		// os.write(START_ARRAY);
		writeGenericObjectWithLength(mv,"START_ARRAY",1);
	}
	
	private void writeCollectionProperty(ASMContextSerializer context,MetaPropertyCollection property, MethodVisitor mv) {
		
		// if (properties.contains(<property.name>) && pojo.get<property.name>() != null && !pojo.get<property.name>().isEmpty()) {
		// 		if (needComma) {
		//			os.write(COMMA);
		//		}
		//		os.write(START);
		//		os.write(<property.name>);
		//		os.write(END_PRO);
		//		os.write(START_ARRAY);
		//		for (PojoSimpleChild child : pojo.getChilds()) {
		//			child_serializer.serialize(os, child,filter);
		//		}
		//		os.write(END_ARRAY);
		//	}
		
		final Label label = new Label();
		
		checkContainsProperty(mv, property.getLabel(), label);
		checkNotNull(mv, property.getName(), label, context.getEntity().getEntityClass(), property.getReturnType());
		checkIsEmpty(mv, property.getName(), label, context.getEntity().getEntityClass(), property.getReturnType());
		checkComma(context, mv);
		
		writeStartArray(context, mv, property.getName());
		
		// for (E e : pojo.getE()) 
		mv.visitVarInsn(Opcodes.ALOAD, 2);
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(context.getEntity().getEntityClass()), ClassUtils.getGetter(property.getName()), "()" + Type.getDescriptor(property.getReturnType()));
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(property.getReturnType()), "iterator", "()Ljava/util/Iterator;");
		final int count = context.incrementAndGetStack();
		mv.visitVarInsn(Opcodes.ASTORE, count);
		final Label labelGOTO = new Label();
		final Label labelFOR = new Label();
		mv.visitJumpInsn(Opcodes.GOTO, labelGOTO);
		mv.visitLabel(labelFOR);
		
		// next()...
		mv.visitVarInsn(Opcodes.ALOAD, count);
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Iterator.class), "next", "()Ljava/lang/Object;");
		mv.visitTypeInsn(Opcodes.CHECKCAST,Type.getInternalName(property.getCollectionItemClass()));
		
		int next = context.incrementAndGetStack();
		mv.visitVarInsn(Opcodes.ASTORE, next);
		
		
		if (property.getCollectionItemClass().isInterface()) {
			
			System.out.println("--------------------------------> INTERFACE line 653");
			
		} else if (property.getCollectionItemClass().equals(Object.class)) {
			
			System.out.println("--------------------------------> OBJECT line 657");
			
		} else {
			
			// collection item is a lead
			final Entity targetEntity = findEntityClass(property.getCollectionItemClass());
			if (!getRegistry().hasChild(targetEntity)) {
				mv.visitVarInsn(Opcodes.ALOAD, 0);
				
				if (property.getCollectionItemSerializer() != null) {
					mv.visitFieldInsn(Opcodes.GETFIELD, context.getClassName(),  property.getCollectionItemClass().getSimpleName().toLowerCase()+FIELD_SUFFIX,Type.getDescriptor(property.getCollectionItemSerializer()));
					mv.visitVarInsn(Opcodes.ALOAD, 1);
					mv.visitVarInsn(Opcodes.ALOAD, next);
				    mv.visitVarInsn(Opcodes.ALOAD, 3);
					mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(property.getCollectionItemSerializer()), "serialize", 
							Type.getMethodDescriptor(TYPE_VOID, TYPE_OUTPUTSTREAM,Type.getType(targetEntity.getEntityClass()),TYPE_FILTER));
				} else {
					mv.visitFieldInsn(Opcodes.GETFIELD, context.getClassName(),  property.getCollectionItemClass().getSimpleName().toLowerCase()+FIELD_SUFFIX,ASMUtil.getDescriptorForSerializer(targetEntity));
					mv.visitVarInsn(Opcodes.ALOAD, 1);
					mv.visitVarInsn(Opcodes.ALOAD, next);
					mv.visitVarInsn(Opcodes.ALOAD, 3);
					mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, targetEntity.getInternalClassNameForSerializer(), "serialize", 
							Type.getMethodDescriptor(TYPE_VOID, TYPE_OUTPUTSTREAM,Type.getType(targetEntity.getEntityClass()),TYPE_FILTER));
				}
				
			} else {
				mv.visitVarInsn(Opcodes.ALOAD, 0);
				mv.visitVarInsn(Opcodes.ALOAD, next);
				mv.visitVarInsn(Opcodes.ALOAD, 1);
				mv.visitVarInsn(Opcodes.ALOAD, 3);
				mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, context.getClassName(), property.getName(),
						Type.getMethodDescriptor(TYPE_VOID, Type.getType(property.getCollectionItemClass()),Type.getType(OutputStream.class), TYPE_FILTER));
				
			}
			
		}
		
		checkIteratorNeedComma(mv,count);
		
		// hasNext() ?
		mv.visitLabel(labelGOTO);
		mv.visitVarInsn(Opcodes.ALOAD, count);
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Iterator.class), "hasNext", "()Z");
		mv.visitJumpInsn(Opcodes.IFNE, labelFOR);
		
		// os.write(END_ARRAY);
		writeGenericObjectWithLength(mv,"END_ARRAY",1);
		mv.visitInsn(Opcodes.ICONST_1);
		mv.visitVarInsn(Opcodes.ISTORE, context.getFlagCount());
		
		mv.visitLabel(label);
	}

	private void writeMapProperty(ASMContextSerializer context,MetaPropertyMap property, MethodVisitor mv) {
		final Label label = new Label();
		
		checkContainsProperty(mv, property.getLabel(), label);
		checkNotNull(mv, property.getName(), label, context.getEntity().getEntityClass(), property.getReturnType());
		checkIsEmpty(mv, property.getName(), label, context.getEntity().getEntityClass(), property.getReturnType());
		checkComma(context, mv);
		
		writeGenericObjectWithLength(mv,"DOUBLE_QUOTE",1);
		writeStaticField(mv,context,"FIELD_BYTE_" + property.getName().toUpperCase());
		writeGenericObjectWithLength(mv,"PROPERTY_END",2);
		writeGenericObjectWithLength(mv,"START_OBJECT",1);

		mv.visitVarInsn(Opcodes.ALOAD, 2);
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(context.getEntity().getEntityClass()), ClassUtils.getGetter(property.getName()), "()" + Type.getDescriptor(property.getReturnType()));
		final int mapIndex = context.incrementAndGetStack();
		mv.visitVarInsn(Opcodes.ASTORE, mapIndex);
		mv.visitVarInsn(Opcodes.ALOAD, mapIndex);
		
		
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, TYPE_MAP.getInternalName(), "keySet", "()Ljava/util/Set;");
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, TYPE_SET.getInternalName(), "iterator", "()Ljava/util/Iterator;");
		final int count = context.incrementAndGetStack();
		mv.visitVarInsn(Opcodes.ASTORE, count);
		
		final Label labelGOTO = new Label();
		final Label labelFOR = new Label();
		mv.visitJumpInsn(Opcodes.GOTO, labelGOTO);
		mv.visitLabel(labelFOR);
		
		// next()...
		mv.visitVarInsn(Opcodes.ALOAD, count);
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Iterator.class), "next", "()Ljava/lang/Object;");
		
		final int keyIndex = context.incrementAndGetStack();
		final int valueIndex = context.incrementAndGetStack();
		mv.visitVarInsn(Opcodes.ASTORE, keyIndex);
		
		mv.visitVarInsn(Opcodes.ALOAD, mapIndex);
		mv.visitVarInsn(Opcodes.ALOAD, keyIndex);
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, TYPE_MAP.getInternalName(), "get", Type.getMethodDescriptor(TYPE_OBJECT,TYPE_OBJECT));
		mv.visitVarInsn(Opcodes.ASTORE, valueIndex);
		
		
		writeMapItem(context,mv,property,keyIndex,valueIndex);

		checkIteratorNeedComma(mv,count);
		
		// hasNext() ?
		mv.visitLabel(labelGOTO);
		mv.visitVarInsn(Opcodes.ALOAD, count);
		mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, Type.getInternalName(Iterator.class), "hasNext", "()Z");
		mv.visitJumpInsn(Opcodes.IFNE, labelFOR);
	 
		mv.visitInsn(Opcodes.ICONST_1);
		mv.visitVarInsn(Opcodes.ISTORE, context.getFlagCount());

		mv.visitLabel(label);
		
		writeGenericObjectWithLength(mv,"END_OBJECT",1);
	}
	
	
	private static void encodeToString(MethodVisitor mv,int index) {
		mv.visitVarInsn(Opcodes.ALOAD, index);
		mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, TYPE_OBJECT.getInternalName(), "toString", Type.getMethodDescriptor(TYPE_STRING));
		mv.visitVarInsn(Opcodes.ALOAD, 1);
		mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(ASMSerializer.class), "encode", 
				Type.getMethodDescriptor(TYPE_VOID,TYPE_STRING,TYPE_OUTPUTSTREAM));
	}
	
	private void writeMapItem(ASMContextSerializer context,MethodVisitor mv, MetaPropertyMap property,int keyIndex, int valueIndex) {

		final Class<?> keyClass = property.getKeyItem();
		final Class<?> valueClass = property.getValueItem();

		if (ASMUtil.isSingleValue(keyClass)) {
			
			if (String.class.equals(keyClass) || Enum.class.isAssignableFrom(keyClass) || Number.class.isAssignableFrom(keyClass)) {
				writeGenericObjectWithLength(mv,"DOUBLE_QUOTE",1);
				encodeToString(mv,keyIndex);
				writeGenericObjectWithLength(mv,"PROPERTY_END",2);
				
			} else {
				throw new JSONException("Map item Key is not supported : [" + keyClass + "]");	
			}
		}
		
		if (ASMUtil.isSingleValue(valueClass)) {
			
			if (String.class.equals(valueClass) || Enum.class.isAssignableFrom(valueClass)) {
				writeGenericObjectWithLength(mv,"DOUBLE_QUOTE",1);
				encodeToString(mv,valueIndex);
				writeGenericObjectWithLength(mv,"DOUBLE_QUOTE",1);
				return;
			}
			
			if (Number.class.isAssignableFrom(valueClass)) {
				encodeToString(mv,valueIndex);
				return;
			}
			
			throw new JSONException("Map item Value is not supported : [" + valueClass + "]");
		} else {
			final Entity targetEntity = findEntityClass(valueClass);
			mv.visitVarInsn(Opcodes.ALOAD, 0);
			mv.visitFieldInsn(Opcodes.GETFIELD, context.getClassName(), valueClass.getSimpleName().toLowerCase()+FIELD_SUFFIX,ASMUtil.getDescriptorForSerializer(targetEntity));
			mv.visitVarInsn(Opcodes.ALOAD, 1);
			mv.visitVarInsn(Opcodes.ALOAD, valueIndex);
			mv.visitVarInsn(Opcodes.ALOAD, 3);
			mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, targetEntity.getInternalClassNameForSerializer(), "serialize",Type.getMethodDescriptor(TYPE_VOID,TYPE_OUTPUTSTREAM, TYPE_OBJECT, TYPE_FILTER));
		}
		
	}
	
	private final class GenerateMethod extends MethodVisitorTemplate<ASMContextSerializer> {
		public GenerateMethod(ASMContextSerializer context) {
			super(Opcodes.ACC_PUBLIC,
					"serialize",
					Type.getMethodDescriptor(TYPE_VOID, TYPE_OUTPUTSTREAM,Type.getType(context.getEntity().getEntityClass()),TYPE_FILTER),	
					null,
					new String[]{Type.getInternalName(IOException.class)});
			setContext(context);
		}
		/**
		 * @see org.manon.json.asm.MethodVisitorTemplate#doCreate(org.manon.json.asm.ASMContextSerializer, org.objectweb.asm.MethodVisitor)
		 */
		@Override
		protected void doCreate(ASMContextSerializer context, MethodVisitor methodVisitor) throws Exception {
			final String[] path = ClassUtils.path(context.getEntity().getEntityClass());
			final TreeNode<Entity, String> node = getRegistry().searchPath(path);
			
			writeGetPropertiesToFilter(context,methodVisitor);
			writeGenericObjectWithLength(methodVisitor,"START_OBJECT",1);
			
			if (path.length > 1) {
				
				final Entity parent = getRegistry().search(Arrays.copyOfRange(path, 0, path.length-1));
				
				// call super;
				methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
				methodVisitor.visitVarInsn(Opcodes.ALOAD, 1);
				methodVisitor.visitVarInsn(Opcodes.ALOAD, 2);
				methodVisitor.visitVarInsn(Opcodes.ALOAD, 3);
				methodVisitor.visitVarInsn(Opcodes.ALOAD, 4);
				methodVisitor.visitInsn(Opcodes.ICONST_0);
				methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,parent.getInternalClassNameForSerializer(),"innerSerialize",
						Type.getMethodDescriptor(TYPE_BOOLEAN, TYPE_OUTPUTSTREAM,Type.getType(parent.getEntityClass()),TYPE_FILTER,TYPE_SET,TYPE_BOOLEAN));
				methodVisitor.visitVarInsn(Opcodes.ISTORE, 5);
			} else {
				methodVisitor.visitInsn(Opcodes.ICONST_0);
				methodVisitor.visitVarInsn(Opcodes.ISTORE, 5);
			}
			
			if (!node.isLeaf()) {
				generateMetohdInner(context);
			}
			
			context.setStackCount(4);
			writeProperties(context,methodVisitor,false);
			writeGenericObjectWithLength(methodVisitor,"END_OBJECT",1);
		}
	}
	
}